package plp.functional2.expression;

import static java.util.Arrays.asList;
import static plp.expressions1.util.ToStringProvider.listToString;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import plp.expressions1.util.Tipo;
import plp.expressions2.expression.Expressao;
import plp.expressions2.expression.Id;
import plp.expressions2.expression.Valor;
import plp.expressions2.expression.ValorInteiro;
import plp.expressions2.memory.AmbienteCompilacao;
import plp.expressions2.memory.AmbienteExecucao;
import plp.expressions2.memory.ContextoExecucao;
import plp.expressions2.memory.VariavelJaDeclaradaException;
import plp.expressions2.memory.VariavelNaoDeclaradaException;
import plp.functional1.util.TipoFuncao;
import plp.functional1.util.TipoPolimorfico;

public class Aplicacao implements Expressao {

	private Expressao func;
	private List<? extends Expressao> argsExpressao;

	public Aplicacao() {
	}

	public Aplicacao(Expressao f, Expressao... expressoes) {
		this(f, asList(expressoes));
	}

	public Aplicacao(Expressao f, List<? extends Expressao> expressoes) {
		func = f;
		argsExpressao = expressoes;
	}

	public Valor avaliar(AmbienteExecucao ambiente)
	throws VariavelNaoDeclaradaException, VariavelJaDeclaradaException {

		Valor vresult = (Valor) new ValorInteiro(0);
		
		Valor valor = func.avaliar(ambiente);		

		if (valor instanceof ValorFuncao) {

			Map<Id, Valor> mapIdValor = resolveParametersBindings(ambiente,	((ValorFuncao)valor));

			ambiente.incrementa();

			includeValueBindings(ambiente, mapIdValor);

			Expressao exp = ((ValorFuncao)valor).getExp();
			vresult = exp.avaliar(ambiente);

			ambiente.restaura();
		} else {

			ContextoExecucao contexto = null;

			try {
				contexto = ((ContextoExecucao) ambiente).clone();
			} catch (CloneNotSupportedException e) {
				e.printStackTrace();
			}

			ProcAplicacao processo = new ProcAplicacao(	((AmbienteExecucao) contexto), func, argsExpressao);
			Thread thread = new Thread(processo);
			thread.start();
		}

		return vresult;
	}

	/**
	 * Realiza a verificacao de tipos desta expressao.
	 * 
	 * @param amb
	 *            o ambiente de compila��o.
	 * @return <code>true</code> se os tipos da expressao sao validos;
	 *         <code>false</code> caso contrario.
	 * @exception VariavelNaoDeclaradaException
	 *                se existir um identificador nao declarado no ambiente.
	 * @exception VariavelNaoDeclaradaException
	 *                se existir um identificador declarado mais de uma vez no
	 *                mesmo bloco do ambiente.
	 */
	public boolean checaTipo(AmbienteCompilacao ambiente)
	throws VariavelNaoDeclaradaException, VariavelJaDeclaradaException {
		Tipo tipo = getFuncType(ambiente);

		boolean result;
		
		TipoFuncao tipoFuncao = (TipoFuncao) tipo;
		result = tipoFuncao.checaTipo(ambiente, argsExpressao);

		return result;
	}

	private Tipo getFuncType(AmbienteCompilacao ambiente) {
		Tipo tipoFuncao = null;
		if (func instanceof Id) {
			tipoFuncao = ambiente.get((Id) func);
		} else if (func instanceof ValorFuncao) {
			tipoFuncao = ((ValorFuncao) func).getTipo(ambiente);
		}
		if (tipoFuncao == null || tipoFuncao instanceof TipoPolimorfico) {
			ArrayList<Tipo> params = new ArrayList<Tipo>();
			for (Expressao valorReal : argsExpressao) {
				params.add(valorReal.getTipo(ambiente));
			}
			tipoFuncao = new TipoFuncao(params, new TipoPolimorfico());
		}
		return tipoFuncao;
	}

	/**
	 * Returns the args.
	 * 
	 * @return ListaExpressao
	 */
	public List<? extends Expressao> getArgsExpressao() {
		return argsExpressao;
	}

	/**
	 * Returns the func.
	 * 
	 * @return Id
	 */
	public Expressao getFunc() {
		return func;
	}

	/**
	 * Retorna os tipos possiveis desta expressao.
	 * 
	 * @param amb
	 *            o ambiente de compila��o.
	 * @return os tipos possiveis desta expressao.
	 * @exception VariavelNaoDeclaradaException
	 *                se existir um identificador nao declarado no ambiente.
	 * @exception VariavelNaoDeclaradaException
	 *                se existir um identificador declarado mais de uma vez no
	 *                mesmo bloco do ambiente.
	 * @precondition this.checaTipo();
	 */
	public Tipo getTipo(AmbienteCompilacao ambiente)
	throws VariavelNaoDeclaradaException, VariavelJaDeclaradaException {

		Tipo tipo = getFuncType(ambiente);

		TipoFuncao tipoFuncao = (TipoFuncao) tipo;

		return tipoFuncao.getTipo(ambiente, argsExpressao);
	}

	private void includeValueBindings(AmbienteExecucao ambiente,
			Map<Id, Valor> mapIdValor) throws VariavelJaDeclaradaException {
		for (Map.Entry<Id, Valor> mapeamento : mapIdValor.entrySet()) {
			Id id = mapeamento.getKey();
			Valor valor = mapeamento.getValue();
			ambiente.map(id, valor);
		}
	}

	protected Map<Id, Valor> resolveParametersBindings(AmbienteExecucao ambiente,
			ValorFuncao funcao) throws VariavelNaoDeclaradaException,
			VariavelJaDeclaradaException {
		List<Id> parametrosId = funcao.getListaId();
		List<? extends Expressao> expressoesValorReal = argsExpressao;

		Map<Id, Valor> mapIdValor = new HashMap<Id, Valor>();

		Iterator<? extends Expressao> iterExpressoesValor = expressoesValorReal
		.iterator();
		for (Id id : parametrosId) {
			Expressao exp = iterExpressoesValor.next();
			Valor valorReal = exp.avaliar(ambiente);
			mapIdValor.put(id, valorReal);
		}

		return mapIdValor;
	}

	/**
	 * Retorna uma representacao String desta expressao. Util para depuracao.
	 * 
	 * @return uma representacao String desta expressao.
	 */
	@Override
	public String toString() {
		return String.format("%s(%s)", func, listToString(argsExpressao, ","));
	}

	public void setFunc(Expressao func) {
		this.func = func;
	}

	public void setArgsExpressao(List<? extends Expressao> argsExpressao) {
		this.argsExpressao = argsExpressao;
	}

	public void setArgsExpressao(Expressao ... argsExpressao) {
		this.argsExpressao = Arrays.asList(argsExpressao);
	}

}
